OC Runtime之动态方法解析和消息转发

首先直接上代码:

@interface Person : NSObject  
- (void)eat;  
- (void)say:(NSString*)world;  
@end  

@implementation AA  
- (void)eat {  
  NSLog(@"eat");  
}  
@end
int main (int argc, const charchar * argv[]) {   
    @autoreleasepool {  
        Person * p = [Person new];   
        [p eat];  
        [p say:@”Hello”];    
    }   
    return 0;   
}  

以上代码执行后会产生什么样的结果?我想大家都应该很清楚,那就是程序会crash,因为我们并未实现方法say:,在Person及其父类的method list中都找不到其相应实现,所以会crash。错误原因:-[Person say:]: unrecognized selector sent to instance 0x100100020

那么怎么解决这样的问题呢,答案就是动态方法解析(Dynamic Method Resolution)和消息转发(Message Forwarding)。

动态方法解析(Dynamic Method Resolution)

你可以动态地提供一个方法的实现。例如我们可以用@dynamic关键字在类的实现文件中修饰一个属性:

@dynamic propertyName;

关键字dynamic的作用是告诉编译器与属性相关的方法将在运行时动态提供,编译器不需生成对应的getter setter方法(原文:tells the compiler that the methods associated with the property will be provided dynamically)。我们可以通过分别重载resolveInstanceMethod:和resolveClassMethod:方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:或resolveClassMethod:来给程序员一次动态添加方法实现的机会。我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector: 或 instancesRespondToSelector:方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。如果你实想让该方法选择器通过转发机制转发,那么就让resolveInstanceMethod:返回NO。

消息转发(Message Forwarding)

给某个对象发送无法处理的消息时会产生错误。幸运的是在错误报出之前,runtime系统给了这个对象第二次处理这个消息的机会(the runtime system gives the receiving object a second chance to handle the message)。

  1. 重定向
    在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其他对象:

    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        if(aSelector == @selector(mysteriousMethod:)){
            return alternateObject;
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    

    如果此方法返回nil或self,则会进入消息转发机制(forwardInvocation:);否则将向返回的对象重新发送消息。

  2. 转发
    当动态方法解析不作处理返回NO时,消息转发机制会被触发。在这时forwardInvocation:方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:

    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {
        if ([someOtherObject respondsToSelector:
                [anInvocation selector]])
            [anInvocation invokeWithTarget:someOtherObject];
        else
            [super forwardInvocation:anInvocation];
    }
    

    该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。

    这里需要注意的是参数anInvocation是从哪的来的呢?其实在forwardInvocation:消息发送前,Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以我们在重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,否则会抛异常。

    当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都从NSObject类中继承了forwardInvocation:方法。然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:。通过实现我们自己的forwardInvocation:方法,我们可以在该方法实现中将消息转发给其它对象。

    forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。

    注意: forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:将不可能会被调用。

整个消息处理过程如下图所示:
img

步骤1、2、3都是在没有实现msg方法的情况下,runtime为我们提供的补救的机会。
针对文章开始崩溃的问题,可以用三种方式补救:

第一种:resolveInstanceMethod:

// c形式
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(say:)) {
        class_addMethod([self class], sel, (IMP)say, "v@:*");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void say(id self, SEL _cmd, NSString *str) {
    NSLog(@"Person say:%@", str);
}

//OC形式
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(say:)) {
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(sayMethodIMP:)), "v@:*");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)sayMethodIMP:(NSString *)str {
    NSLog(@"Person say:%@", str);
}

第二种:forwardingTargetForSelector:这里增加了Mobile类来相应say:消息。

//Mobile.h
#import <Foundation/Foundation.h>

@interface Mobile : NSObject
- (void)say:(NSString*)world;
@end

//Mobile.m
#import "Mobile.h"

@implementation Mobile
- (void)say:(NSString*)world {
    NSLog(@"Mobile say:%@", world);
}
@end

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(say:)) {
        return [Mobile new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

第三种:forwardInvocation:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(say:)) {
        return [NSMethodSignature signatureWithObjCTypes:"V@:*"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    Mobile *mobile = [Mobile new];
    if ([mobile respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:mobile];
    }
}

参考

http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/

ObjCRuntimeGuide